/*
 * Copyright (c) 2009 John Pritchard, WTKX Project Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package wtkx;

import wtkx.in.LocalManifest;
import wtkx.ui.Border;
import wtkx.ui.Caret;
import wtkx.ui.Clipboard;
import wtkx.ui.Threads;
import wtkx.ui.Direction;
import wtkx.ui.GV;
import wtkx.ui.Insets;
import wtkx.ui.Keyboard;
import wtkx.ui.Layout;
import wtkx.ui.Manifest;
import wtkx.ui.Mouse;
import wtkx.ui.Platform;
import wtkx.ui.Password;
import wtkx.ui.Position;
import wtkx.ui.Selection;
import wtkx.ui.TextListener;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.nio.charset.Charset;
import javax.xml.stream.Location;


/**
 * Text layout, typically employed in one of its subclasses.  
 * @see P
 * @see Textarea
 * @see Input
 * @see Label
 * @author jdp
 */
public class Text 
    extends Component
    implements jbxml.Component.AddText, 
               Position.TextComponent,
               Runnable
{
    public final static Charset UTF8 = Charset.forName("UTF-8");

    public enum Decoration {
        NONE,
        UNDERLINE,
        STRIKETHROUGH;

        public static Decoration decode(String value) {
            return valueOf(value.toUpperCase());
        }
    }


    private StringBuilder buffer = new StringBuilder();

    protected volatile boolean editable;
    protected volatile boolean password;
    protected volatile String text;
    protected volatile boolean line;
    protected volatile boolean flowed;
    protected volatile boolean textChanged;

    protected volatile int rows, columns;

    protected volatile GV glyphs;

    protected final Position position;

    protected volatile Caret caret;

    protected volatile Selection selection;

    protected volatile TextListener listener;

    protected volatile boolean caretOn = true;

    protected volatile int caretRepaintIntervalID = -1;

    protected volatile boolean mouseSelected;


    public Text(){
        super();
        this.caretOn = true;
        this.textChanged = true;
        this.focusableComponent = true;
        this.position = new Position(this);
    }


    public Text clone(){
        Text clone = (Text)super.clone();
        clone.buffer = new StringBuilder(this.buffer.toString());//[TODO] (Position)
        return clone();
    }
    public void destroy(){
        this.position.destroy();
        this.listener = null;
        super.destroy();
    }
    public TextListener getTextListener(){
        return this.listener;
    }
    public void setTextListener(TextListener listener){
        this.listener = listener;
    }
    public StringBuilder textBuffer(){
        return this.buffer;
    }
    public GV textGlyphs(){
        return this.glyphs;
    }
    public Caret textCaret(){
        return this.caret;
    }
    protected void layoutComponent(){
        super.layout();
    }
    public void layout(){

        FontMetrics fm = this.getFontMetrics();
        if (null != fm){
            int rows = this.rows;
            if (0 < rows){
                this.setInnerHeight(rows * fm.getHeight());
            }
            int cols = this.columns;
            if (0 < cols){
                this.setInnerWidth( (cols * fm.charWidth(0xffff)));
            }
        }
        super.layout();
        this.layoutText();
    }
    protected void layoutText(){
        FontMetrics fm = this.getFontMetrics();
        if (null != fm){
            boolean textChanged = this.textChanged;
            if (textChanged || (!this.valid)){
                this.textChanged = false;

                Font font = this.getFont();

                String text = this.getDisplayText();

                Insets padding = this.padding;
                float ix = padding.left;
                float iy = padding.top;
                float iw = this.getInnerWidth();
                float ih = this.getInnerHeight();

                /*
                 * display: block;
                 */
                this.glyphs = new GV(text,font,fm,line,flowed,ix,iy,iw,ih);

                if (1 != this.rows){
                    Selection selectAll = new Selection(this.glyphs,0,this.getCharacterCount());
                    Rectangle2D.Float glyphBounds = (Rectangle2D.Float)selectAll.getBounds2D();
                    this.layoutView(glyphBounds);
                }

                this.caret = new Caret(this.glyphs,this.position);
            }
            else if (this.position.isNot(this.caret))
                this.caret = new Caret(this.glyphs,this.position);

            boolean selectionChanged = this.position.unsetChanged();
            boolean selectionPositive = this.position.isNotEmpty();

            if (selectionChanged){

                if (selectionPositive){

                    if (textChanged || this.position.isNot(this.selection))
                        this.selection = new Selection(this.glyphs,this.position);

                    if (null != this.scrolling)
                        this.scrolling.see(this.selection,this.glyphs.lineheight);
                }
                else {
                    this.selection = null;

                    if (null != this.scrolling)
                        this.scrolling.see(this.caret,this.glyphs.lineheight);
                }
            }
            else if (null != this.scrolling){
                this.scrolling.see(this.caret,this.glyphs.lineheight);
            }
        }
        else {
            this.selection = null;
        }
    }
    public void paint(Graphics2D g){

        if (this.border.isNotEmpty())
            this.border.fill(this,g);

        if (this.textChanged || this.position.isChanged())
            this.layoutText();


        if (null != this.scrolling){

            float dy = this.scrolling.dy;
            if (0f == dy)
                this.paintText(g);
            else {
                Graphics2D gs = (Graphics2D)g.create();
                try {
                    gs.translate(0f,-dy);

                    this.paintText(gs);
                }
                finally {
                    gs.dispose();
                }
            }
        }
        else {
            this.paintText(g);
        }
    }
    protected void paintText(Graphics2D g){

        Shape selection = this.selection;
        if (null != selection){
            Color sc = this.getSelectionColor();
            if (null != sc){
                g.setColor(sc);
                g.fill(selection);
            }
        }

        GV glyphs = this.glyphs;
        if (null != glyphs){
            Color color = this.getColor();
            if (null != color)
                g.setColor(color);

            g.setStroke(DefaultStroke);

            Insets padding = this.padding;

            glyphs.paint(g,padding.left,padding.top);

            if (this.editable && this.caretOn && null == selection){
                Shape caret = this.caret;
                if (null != caret)
                    g.draw(caret);
            }
        }
    }

    public boolean isEditable() {
        return editable;
    }
    public void setEditable(boolean editable) {
        this.caretOn = editable;
        this.editable = editable;

        this.caretDisplay(editable);
    }
    public boolean isFlowed() {
        return flowed;
    }
    public void setFlowed(boolean flowed) {
        this.flowed = flowed;
    }
    public boolean hasRows(){
        return (0 < this.rows);
    }
    public boolean hasNotRows(){
        return (0 == this.rows);
    }
    public int getRows(){
        return this.rows;
    }
    public void setRows(int r){
        if (-1 < r)
            this.rows = r;
        else
            throw new IllegalArgumentException(String.valueOf(r));
    }
    public void setRows(String s){
        try {
            this.rows = Integer.parseInt(s);
        }
        catch (NumberFormatException exc){
            if ("auto".equals(s))
                this.rows = 0;
            else
                throw exc;
        }
    }
    public boolean hasColumns(){
        return (0 < this.columns);
    }
    public boolean hasNotColumns(){
        return (0 == this.columns);
    }
    public int getColumns(){
        return this.columns;
    }
    public void setColumns(int c){
        if (-1 < c)
            this.columns = c;
        else
            throw new IllegalArgumentException(String.valueOf(c));
    }
    public final String getDisplayText(){
        if (this.password)
            return Password.Get(this.buffer.length());
        else
            return this.buffer.toString();
    }
    public final String getText(){

        return this.buffer.toString();
    }
    public final boolean hasText(){
        return (0 < this.buffer.length());
    }
    public final boolean hasTextLengthGe(int len){
        return (len <= this.buffer.length());
    }
    public final boolean hasTextLines(){
        return (-1 < this.buffer.indexOf("\n"));
    }
    public final boolean isText(String text){
        if (null == text || 1 > text.length())
            return (0 == buffer.length());
        else 
            return (text.equals(this.buffer.toString()));
    }
    public final int getCharacterCount(){

        return this.buffer.length();
    }
    public final String getSelectedText(){

        Position p = this.position;
        int start = p.getOffset();
        int end = p.getEnd();
        if (-1 < start && 0 < end)
            return this.buffer.substring(start,end);
        else
            return "";
    }
    /**
     * @see wtkx.io.reader.Component$Text
     */
    public void addElementText(Location loc, String text){
        this.appendText(text);
    }
    public final boolean setText(String text){

        if (null != text && 0 < text.length()){
            this.buffer.setLength(0);
            this.buffer.append(text);

            this.position.init();

            this.textChanged = true;

            if (null != this.listener)
                this.listener.charactersAppended(text);

            this.onchange(this);

            this.repaint();

            return true;
        }
        else {
            int count = this.buffer.length();
            if (0 < count){
                this.buffer.setLength(0);

                this.position.init();

                this.textChanged = true;

                if (null != this.listener)
                    this.listener.charactersRemoved(0,count);

                this.onchange(this);

                this.repaint();

                return true;
            }
            else
                return false;
        }
    }
    public final boolean appendText(char ch){
        return this.appendText(String.valueOf(ch));
    }
    public final boolean appendText(String text){

        if (this.position.append(text,this.glyphs,this.listener)){

            this.textChanged = true;

            this.onchange(this);

            this.repaint();

            return true;
        }
        else
            return false;
    }
    public void insertParagraph(){
    }
    public final boolean insertText(char ch) {
        return this.insertText(Character.toString(ch));
    }
    public final boolean insertText(String text) {
        if (null != text && 0 < text.length()){

            if (this.position.isNotEmpty()){

                return this.replaceText(text);
            }
            else if (this.position.insert(text,this.glyphs,this.listener)){

                this.textChanged = true;

                this.onchange(this);

                this.repaint();

                return true;
            }
        }
        return false;
    }
    public final boolean insertText(int ofs, String string) {

        if (null != string){
            int strlen = string.length();
            if (0 < strlen){

                this.buffer.insert(ofs,string);

                this.position.insert(ofs,strlen);

                if (null != this.listener)
                    this.listener.charactersInserted(ofs,string);

                this.textChanged = true;

                this.onchange(this);

                this.repaint();

                return true;
            }
        }
        return false;
    }
    public final String deleteText(Direction direction) {

        String re = this.position.delete(direction,this.glyphs,this.listener);
        if (null != re){

            this.textChanged = true;

            this.onchange(this);

            this.repaint();
        }
        return re;
    }
    public final String deleteText(int start, int end){

        String cut = this.buffer.substring(start,end);
        if (null != cut){

            this.buffer.delete(start,end);

            this.position.delete(start,end);

            if (null != listener)
                listener.charactersRemoved(start,end);

            this.textChanged = true;

            this.onchange(this);

            this.repaint();
        }
        return cut;
    }
    public final boolean replaceText(String string){

        if (this.position.replace(string,this.glyphs,this.listener)){

            this.textChanged = true;

            this.onchange(this);

            this.repaint();

            return true;
        }
        else
            return false;
    }
    public final boolean replaceText(int start, int end, String string){

        int buflen = this.buffer.length();
        if (-1 < start && start < end && end <= buflen){
            if (null != string){
                int strlen = string.length();
                if (0 < strlen){

                    this.buffer.replace(start,end,string);

                    this.position.replace(start,end,strlen);

                    this.textChanged = true;

                    if (null != this.listener)
                        this.listener.charactersReplaced(start,end,string);

                    this.onchange(this);

                    this.repaint();

                    return true;
                }
            }
        }
        return false;
    }
    public final String cut() {

        if (this.position.isNotEmpty()){

            String re = this.deleteText(Direction.Forward);
            if (null != re){
                LocalManifest clipboardContent = new LocalManifest();
                clipboardContent.putText(re);
                Clipboard.setContent(clipboardContent);
            }
            return re;
        }
        else
            return null;
    }
    public final String copy(){
        String copy = this.getSelectedText();
        if (null != copy) {
            LocalManifest clipboardContent = new LocalManifest();
            clipboardContent.putText(copy);
            Clipboard.setContent(clipboardContent);
        }
        return copy;
    }
    public final String paste() {

        Manifest clipboardContent = Clipboard.getContent();
        if (clipboardContent != null && clipboardContent.containsText()) {
            try {
                String text = clipboardContent.getText();
                if (text != null) {

                    if (this.position.isNotEmpty()){

                        this.replaceText(text);
                    }
                    else {
                        this.insertText(text);
                    }
                }
                return text;
            }
            catch (java.io.IOException ignore) {
            }
        }
        return null;
    }
    public final void undo() {
    }
    public final void redo() {
    }
    public void focusChanged(boolean in){

        this.caretDisplay(in);

        super.focusChanged(in);
    }
    protected final void caretRepaint(boolean upd){
        Caret caret = this.caret;
        if (null != caret){
            if (upd)
                this.caretOn = (!this.caretOn);

            float x = caret.x;
            float y = caret.y;
            float w = caret.width;
            float h = caret.height;
            if (null != this.scrolling)
                y -= this.scrolling.dy;

            this.repaint(x,y,w,h);
        }
    }
    protected final void caretDisplay(boolean enabled){

        boolean change = this.caretOn;
        this.caretOn = (enabled && this.editable && this.position.isEmpty() && this.isFocused());

        if (this.caretOn){
            if (this.caretRepaintIntervalID == -1) {

                this.caretRepaint(false);

                this.caretRepaintIntervalID = Threads.SetInterval(this, Platform.getCursorBlinkRate());
            }
        } 
        else {
            if (this.caretRepaintIntervalID != -1) {
                Threads.ClearInterval(this.caretRepaintIntervalID);
                this.caretRepaintIntervalID = -1;
            }
            /**
             * Ensure visually clear
             */
            if (change != this.caretOn){
                this.caretRepaint(false);
            }
        }
    }
    protected int getOffsetFor(float x, float y){
        GV glyphs = this.glyphs;
        if (null != glyphs)
            return glyphs.getOffsetFor(x,y);
        else
            return -1;
    }
    protected int getOffsetUp(int lines){
        GV glyphs = this.glyphs;
        if (null != glyphs){
            Caret caret = this.caret;
            if (null == caret)
                caret = new Caret(this.glyphs,this.position);

            if (0 >= caret.gvline)
                return caret.position;
            else {
                Insets padding = this.padding;
                float xx = (caret.x + 1f);
                float yy = (caret.y + 1f);
                yy -= (lines*glyphs.lineheight);
                return glyphs.getOffsetFor(xx,yy);
            }
        }
        return -1;
    }
    protected int getOffsetDown(int lines){
        GV glyphs = this.glyphs;
        if (null != glyphs){
            Caret caret = this.caret;
            if (null == caret)
                caret = new Caret(this.glyphs,this.position);

            int term = glyphs.length-1;
            if (term <= caret.gvline)
                return caret.position;
            else {
                Insets padding = this.padding;
                float xx = (caret.x + 1f);
                float yy = (caret.y + 1f);
                yy += (lines*glyphs.lineheight);
                return glyphs.getOffsetFor(xx,yy);
            }
        }
        return -1;
    }
    protected int getOffsetToStartOfLine(){

        Caret caret = this.caret;
        if (null == caret)
            caret = new Caret(this.glyphs,this.position);

        return caret.gvcpstart;
    }
    protected int getOffsetToEndOfLine(){

        Caret caret = this.caret;
        if (null == caret)
            caret = new Caret(this.glyphs,this.position);

        return caret.gvcpend;
    }

    public boolean mouseDown(Mouse.Button button, float x, float y) {
        if (this.enabled){
            this.mouseSelected = false;

            if (button == Mouse.Button.LEFT) {

                float sy = y;
                if (null != this.scrolling)
                    sy += this.scrolling.dy;

                int offset = this.getOffsetFor(x, sy);

                if (this.position.mouse(offset)){
                    this.mouseSelected = true;
                    this.repaint();
                }

                this.requestFocus();

                Mouse.capture(this);
            }
        }
        return super.mouseDown(button, x, y);
    }
    public boolean mouseUp(Mouse.Button button, float x, float y) {
        boolean consumed = super.mouseUp(button, x, y);
        if (this.enabled){
            if (this.mouseSelected){
                this.mouseSelected = false;

                float sy = y;
                if (null != this.scrolling)
                    sy += this.scrolling.dy;

                int offset = this.getOffsetFor(x, sy);

                if (this.position.select(offset))
                    this.repaint();
            }
        }

        if (Mouse.getCapturer() == this)
            Mouse.release();

        return consumed;
    }
    public boolean mouseWheel(Mouse.ScrollType scroll, int amount, int rot, float x, float y)
    {
        if (null != this.scrolling){
            GV glyphs = this.glyphs;
            if (null != glyphs){
                if (0 > rot){
                    float move = -(glyphs.lineheight * rot);
                    this.scrolling.home(move);
                }
                else {
                    float move = (glyphs.lineheight * rot);
                    this.scrolling.end(move);
                }
                this.repaint();
            }
            return true;
        }
        else
            return super.mouseWheel(scroll,amount,rot,x,y);
    }
    public boolean keyTyped(char character) {
        boolean consumed = super.keyTyped(character);

        if (this.enabled && this.editable) {

            if (!Character.isISOControl(character)){

                if (this.position.keyTyped(character,this.glyphs,this.listener)){

                    this.textChanged = true;

                    this.onchange(this);

                    this.repaint();

                    return true;
                }
            }
        }
        return consumed;
    }
    public boolean keyPressed(int keyCode, Keyboard.KeyLocation keyLocation) {

        if (this.enabled && this.editable) {

            if (Keyboard.isPressed(Keyboard.Modifier.CTRL)) {

                switch (keyCode){
                case Keyboard.KeyCode.A:

                    if (this.position.select(0,this.getCharacterCount()))
                        this.repaint();

                    return true;
                case Keyboard.KeyCode.X:

                    this.cut();

                    return true;
                case Keyboard.KeyCode.C:

                    this.copy();

                    return true;
                case Keyboard.KeyCode.V:

                    this.paste();

                    return true;
                case Keyboard.KeyCode.Z:

                    if (Keyboard.isPressed(Keyboard.Modifier.SHIFT))
                        this.redo();
                    else 
                        this.undo();

                    return true;
                case Keyboard.KeyCode.HOME:

                    if (this.position.home(0))
                        this.repaint();

                    return true;
                case Keyboard.KeyCode.END:

                    if (this.position.end(this.getCharacterCount()))
                        this.repaint();

                    return true;
                default:
                    break;
                }
            }
            else {
                int offset, end;
                switch (keyCode){
                case Keyboard.KeyCode.ENTER:

                    this.insertParagraph();

                    return true;
                case Keyboard.KeyCode.TAB:

                    if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)){
                        this.transferFocus(Direction.Backward);
                        return true;
                    }
                    else if (this.transfersFocus || Keyboard.isPressed(Keyboard.Modifier.CTRL)){
                        this.transferFocus(Direction.Forward);
                        return true;
                    }
                    else
                        return super.keyPressed(keyCode, keyLocation);

                case Keyboard.KeyCode.DELETE:

                    this.deleteText(Direction.Forward);

                    return true;
                case Keyboard.KeyCode.BACKSPACE:

                    this.deleteText(Direction.Backward);

                    return true;
                case Keyboard.KeyCode.LEFT:

                    if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {


                        if (this.position.selectLeft())
                            this.repaint();
                    }
                    else {
                        if (this.position.left())
                            this.repaint();
                    }
                    return true;

                case Keyboard.KeyCode.RIGHT:

                    if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {

                        if (this.position.selectRight())
                            this.repaint();
                    } 
                    else {
                        if (this.position.right())
                            this.repaint();
                    }
                    return true;

                case Keyboard.KeyCode.UP:

                    offset = this.getOffsetUp(1);

                    if (-1 < offset){

                        if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {

                            if (this.position.selectUp(offset))
                                this.repaint();
                        }
                        else
                            if (this.position.up(offset))
                                this.repaint();
                    }
                    return true;

                case Keyboard.KeyCode.DOWN:

                    offset = this.getOffsetDown(1);

                    if (-1 < offset){

                        if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {

                            if (this.position.selectDown(offset))
                                this.repaint();
                        }
                        else
                            if (this.position.down(offset))
                                this.repaint();
                    }
                    return true;

                case Keyboard.KeyCode.PAGE_UP:

                    offset = this.getOffsetUp(20);

                    if (-1 < offset){

                        if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {

                            if (this.position.selectUp(offset))
                                this.repaint();
                        }
                        else
                            if (this.position.up(offset))
                                this.repaint();
                    }

                    return true;
                case Keyboard.KeyCode.PAGE_DOWN:

                    offset = this.getOffsetDown(20);

                    if (-1 < offset){

                        if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {

                            if (this.position.selectDown(offset))
                                this.repaint();
                        }
                        else
                            if (this.position.down(offset))
                                this.repaint();
                    }

                    return true;
                case Keyboard.KeyCode.HOME:

                    offset = this.getOffsetToStartOfLine();

                    if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {

                        if (this.position.selectUp(offset))
                            this.repaint();
                    }
                    else
                        if (this.position.home(offset))
                            this.repaint();

                    return true;
                case Keyboard.KeyCode.END:

                    end = this.getOffsetToEndOfLine();

                    if (-1 < end){

                        if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {

                            if (this.position.selectDown(end))
                                this.repaint();
                        }
                        else
                            if (this.position.end(end))
                                this.repaint();
                    }

                    return true;
                default:
                    break;
                }
            }
        }
        return super.keyPressed(keyCode, keyLocation);
    }

    public void run() {
        this.caretRepaint(true);
    }
}
